'use strict';

const fs = require('fs')

const path = require('path')

const inquirer = require('inquirer');// 在终端 选择项目或者组件的插件包
const fsExtra = require('fs-extra')
const semver = require('semver')
const userHome = require('user-home')// 拿到用户主目录
const glob = require('glob')//7.2.0 遍历目录 拿到文件列表 
const ejs = require('ejs')

const { Command } = require('@cjx-cli-dev/command')
const Package = require('@cjx-cli-dev/package')
const log = require('@cjx-cli-dev/log');
const { spinnerStart, sleep, execAsync } = require('@cjx-cli-dev/utils')
const getProjectTemplate = require('./getProjectTemplate')

// 选择下载项目还是组件 模版
const TYPE_PROJECT = 'project'
const TYPE_COMPONENT = 'component'

// 选择标准安装还是自定义安装 模版
// 标准安装/normal
const TEMPLATE_TYPE_NORMAL = 'normal'
// 自定义安装
const TEMPLATE_TYPE_CUSTOM = 'custom'

// 命令白名单
const WHITE_COMMAND = ['npm', 'cnpm', 'pnpm']

class InitCommand extends Command {
  init() {
    this.projectName = this._argv[0] || '' // 创建项目 的名称
    this.force = !!this._argv[1].force // 是否强制刷新
    log.verbose('projectName', this.projectName)
    log.verbose('force', this.force)
    // console.log('projectName force: ', this._argv,this.projectName, this.force)
  }

  async exec() {
    try {
      console.log('exec业务逻辑  !!!!!!!!!!!!!!!!!')
      // 1. 准备阶段
      const projectInfo = await this.prepare()
      // console.log(projectInfo)
      if (projectInfo) {
        // 2.下载模版阶段
        log.verbose('projectInfo', projectInfo)
        this.projectInfo = projectInfo
        // console.log('projectInfo', projectInfo)
        await this.downLoadTemplate()
        // 3.安装模版阶段
        await this.installTemplate()
      }
      
    } catch (e) {
      log.error(e.message)
      if (process.env.LOG_LEVEL === 'verbose') {
        // 调试模式打印详细报错
        console.log(e)
      }
      
    }
    

  }

  // 准备阶段
  async prepare() {
    // 0. 判断项目是否存在
    const template = await getProjectTemplate()
    // console.log('template', template)
    if (!template || template.length === 0) {
      throw new Error('模版不存在！')
    }
    this.template = template
    // throw new Error('出错了！')
    const localPath = process.cwd()
    
    // 1. 判断目录是否为空
    const ret = this.ifDirIsEmpty(localPath)
    // console.log('判断目录是否为空: ', ret)
    if (!ret) {
      // 不为空
      // 1.1 询问是否继续创建
      let _ifContinue = false
      if (!this.force) {
        const { ifContinue } = await inquirer.prompt([
          {
            type: 'confirm',
            name: 'ifContinue',
            message: '当前文件夹不为空，是否继续创建项目？（继续创建会清空当前文件夹）'
          }
        ])
        _ifContinue = ifContinue
        // console.log(111, _ifContinue)
        if (!_ifContinue) {
          return null
        }
      }

      // return
      // console.log(4444)
      
      // 2. 是否启动强制更新
      if (_ifContinue || this.force) {
        
        // 清空当前目录
        // 给用户做二次确认
        const { confirmDelete } = await inquirer.prompt({
          type: 'confirm',
          name: 'confirmDelete',
          message: '是否确认清空当前目录'
        })

        if (confirmDelete) {
          fsExtra.emptyDirSync(localPath)
        }
        
      }
    }
    
    // 3. 选择创建项目或组件
    // 4. 获取项目或组件的基本信息
    return this.getProjectInfo()
  }

  // 选择创建项目或组件 获取项目或组件的基本信息
  async getProjectInfo() {
    // 判断用户 填写的项目名称是否合法
    function isValidName(v) {
      return /^[a-zA-Z]+([-][a-zA-Z][a-zA-Z0-9]*|[_][a-zA-Z][a-zA-Z0-9]*|[a-zA-Z0-9])*$/.test(v)
    }
    // 判断用户 填写的项目端口号是否合法
    function isValidPort(v) {
      return /^(6553[0-5]|655[0-4]\d|65[0-4]\d{2}|6[0-4]\d{3}|[1-5]\d{4}|[1-9]\d{0,3}|[0-9])$/.test(v)
    }
    let projectInfo = {}

    // 选择创建项目或组件
    const { type } = await inquirer.prompt({
      type: 'list',
      name: 'type',
      message: '请选择初始化类型',
      default: TYPE_PROJECT,
      choices: [{
        name: '项目',
        value: TYPE_PROJECT,
      }, {
        name: '组件',
        value: TYPE_COMPONENT,
      }]
    })
    log.verbose('type', type)

    const title = type === TYPE_PROJECT ? '项目' : '组件'

    //拿到组件模版 或者项目模版
    this.template = this.template.filter(item => item.tag.includes(type))
    // console.log(111, this.template)

    const projectNamePrompt = {
      type: 'input',
      name: 'projectName',
      message: `请输入${title}名称`,
      default: '',
      validate: function(v) {
        // 1.输入的首字符必须为英文字符
        // 2. 尾字符必须为英文字符或数字， 不能为字符
        // 3. 字符仅允许"-_"
        const done = this.async()
        setTimeout(() => {
          if (!isValidName(v)) {
            done(`请输入合法的${title}名称`)
            return
          }
          done(null, true)
        }, 0)
         
      },
      filter: (v) => {
        return v
      }
    }

    let projectPrompt = []
    // this.projectName
    // 判断用户 用init命令创建项目时 输入的项目名称是否合法
    if (!isValidName(this.projectName)) {
      // 如果不合法就 要填写项目名称
      projectPrompt.push(projectNamePrompt)
    } else {
      // 如果合法 就不用在再填写项目名称了
      projectInfo.projectName = this.projectName
    }

    projectPrompt.push({
      type: 'input',
      name: 'projectVersion',
      message: `请输入${title}版本号`,
      default: '1.0.0',
      validate: function(v) {
        const done = this.async()
        setTimeout(() => {
          if (!(!!semver.valid(v))) {
            done(`请输入合法的${title}版本号`)
            return
          }
          done(null, true)
        }, 0)
        // return !!semver.valid(v)
      },
      filter: (v) => {
        return semver.valid(v) ? semver.valid(v) : v
      }
    },
    {
      type: 'input',
      name: 'projectPort',
      message: `请输入${title}端口号`,
      default: '9527',
      validate: function(v) {
        const done = this.async()
        setTimeout(() => {
          if (!isValidPort(v)) {
            done(`请输入合法的${title}端口号`)
            return
          }
        
          done(null, true)
        }, 0)
        // return !!semver.valid(v)
      },
      filter: (v) => {
        return v
      }
    },
    {
      type: 'list',
      name: 'projectTemplate',
      message: `请选择${title}模版`,
      choices: this.createTemplateChoices()
    })

    // 获取项目或组件的基本信息
    if (type === TYPE_PROJECT) {
      // 项目

      const project = await inquirer.prompt(projectPrompt)
      // console.log(project)
      projectInfo = {
        ...projectInfo,
        type,
        ...project
      }
    } else if (type === TYPE_COMPONENT) {
      // 组件
      const descriptionPrompt = {
        type: 'input',
        name: 'componentDescription',
        message: '请输入组件描述信息',
        default: '',
        validate: function(v) {
          const done = this.async()
          setTimeout(() => {
            if (!v) {
              done(`请输入组件描述信息`)
              return
            }
            done(null, true)
          }, 0)
           
        }
      }
      const component = await inquirer.prompt([...projectPrompt, descriptionPrompt])
      // console.log(project)
      projectInfo = {
        ...projectInfo,
        type,
        ...component
      }

    }

    // 下载的模版的 项目/组件 名称  require('kebab-case')/生成驼峰的项目名称
    projectInfo.className = projectInfo.projectName ? require('kebab-case')(projectInfo.projectName).replace(/^-/, '') : ''
    // 下载的模版的 版本号
    projectInfo.version = projectInfo.projectVersion ? projectInfo.projectVersion : ''

    // 下载的模版的 描述信息
    projectInfo.description = projectInfo.componentDescription ? projectInfo.componentDescription : ''
    projectInfo.port = projectInfo.projectPort ? projectInfo.projectPort : ''
    // console.log(1111, projectInfo)
    return projectInfo
  }

  // 下载模版
  async  downLoadTemplate() {
    // 1. 通过项目模版api 获取项目模版信息
    // 1.1 通过egg.js 搭建一套后端系统
    // 1.2 通过npm 存储项目模版
    // 1.3 将项目模版信息 储存到mongodb数据库中
    // 1.4 通过egg.js 获取到mongodb中的数据并且通过api返回
    
    const { projectTemplate } = this.projectInfo
    const  templateInfo = this.template.find(item => item.npmName === projectTemplate)
    this.templateInfo = templateInfo
    // console.log(this.projectInfo, templateInfo)
    // package包 存储的目标路径
    const targetPath = path.resolve(userHome, '.cjx-cli-dev', 'template')
    // 缓存 package 的路径
    const storeDir = path.resolve(userHome, '.cjx-cli-dev', 'template', 'node_modules')
    // 拿到package包 的 name/包名 和 version/版本号
    const { npmName, version, isOriginal } = templateInfo
    const spinnerName = templateInfo.name
    // console.log(444, templateInfo, JSON.parse(isOriginal))
    const templateNpm = new Package({
      targetPath,
      storeDir,
      packageName: npmName,
      packageVersion: version,
      isOriginal: isOriginal ? JSON.parse(isOriginal) : true
      
    })
    // console.log('templateNpm', templateNpm)
    if (await templateNpm.exists()) {
      // 更新 package包
      // log.verbose('更新package')
      const spinner = spinnerStart(`正在更新${spinnerName}...`)
      await sleep()
      try {
        await templateNpm.update()
      } catch (e) {
        throw e
      } finally {
        spinner.stop(true)// 下载package包 动画效果关闭
        
        log.success('更新模版成功')
        this.templateNpm = templateNpm
        
      }
    } else {
      // 安装 package包
      const spinner = spinnerStart(`正在下载${spinnerName}...`)
      await sleep()
      try {
        await templateNpm.install()
      } catch (e) {
        throw e
      } finally {
        spinner.stop(true)// 下载package包 动画效果关闭
        if (await templateNpm.exists()) {
          log.success('下载模版成功')
          this.templateNpm = templateNpm
        }
        
      }
    }
  }

  // 安装模版
  async installTemplate() {
    
    // type  normal/正常安装 this.templateInfo/选择的模版信息
    if (this.templateInfo) {
      // 默认标准安装
      this.templateInfo.type = this.templateInfo.type ||  TEMPLATE_TYPE_NORMAL
      // log.verbose(444, this.templateInfo.type)
      if (this.templateInfo.type === TEMPLATE_TYPE_NORMAL) {
        // 标准安装
        await this.normalInstallTemplate()
      } else if (this.templateInfo.type === TEMPLATE_TYPE_CUSTOM) {
        // 自定义安装
        await this.customInstallTemplate()
      } else {
        throw new Error('无法识别项目模版类型！')
      }
    } else {
      throw new Error('项目模版信息不存在')
    }
    // console.log('安装模版阶段', this.templateInfo)
  }

  // ejs 模版处理
  async ejsRender(ignore) {
    const dir = process.cwd()
    return new Promise((resolve, reject) => {
      // **/遍历当前整个目录 
      return glob('**', {
        cwd: dir,
        ignore: ignore || '',// 忽略那些文件不遍历
        nodir: true,// 不匹配目录，只匹配文件。（注意：要只匹配目录，只需在模式末尾放一个/即可。）
      }, (err, files) => {
        if (err) {
          reject(err)
        }
        console.log('glob', files)
        Promise.all(files.map(file => {
          const filePath = path.join(dir, file)
          // console.log('ejs:', filePath)
          return new Promise((resolveOne, rejectOne) => {
             // filename/正在渲染的文件的名称
            // ejs.renderFile(filename, data, options, function(err, str){
            //  // str => Rendered HTML string
            // });
           
            ejs.renderFile(filePath, this.projectInfo, {}, (err, result) => {
              // console.log(err)
              if (err) {
                rejectOne(err)
              } else {
                // 找到要用ejs c处理的文件并替换 ejs处理好的文件
                fsExtra.writeFileSync(filePath, result)
                resolveOne(result)
              }
            })
          })
        })).then(() => {
          resolve()
        }).catch(err => {
          reject(err)
        })
        
      })
    })
  }

  // 标准安装 模版
  async normalInstallTemplate() {
    log.verbose('标准安装模版')
    // 拷贝模版代码至当前目录
    let spinner = spinnerStart('正在安装模版...')
    await sleep()
    try {
      
      const templatePath = path.resolve(this.templateNpm.cacheFilePathPackageJson, 'template')
      // 终端打开的文件目录/当前目录
      const targetPath = process.cwd()
      // console.log(666, templatePath)
      // console.log(777, targetPath)
      // console.log(888, this.templateNpm)
      
      // 确保这个目录存在
      fsExtra.ensureDirSync(templatePath)
      fsExtra.ensureDirSync(targetPath)
      // 把缓存目录里的模版拷贝到 终端打开的文件目录/当前目录
      fsExtra.copySync(templatePath, targetPath)
    } catch(e) {
      throw e
    } finally {
      spinner.stop(true)
      log.success('模版安装成功')

      // 下载的模版的 title
      this.projectInfo.CJX_CLI_TEMPLATE_TITLE =  this.templateInfo.CJX_CLI_TEMPLATE_TITLE ? this.templateInfo.CJX_CLI_TEMPLATE_TITLE : 'cjx-cli模版'
      
      const templateIgnore = this.templateInfo.ignore || []
      const ignore = ['node_modules/**', ...templateIgnore]
      //console.log(9999, this.templateInfo)
      
      await this.ejsRender(ignore)

      // return
      const  { installCommand, startCommand } = this.templateInfo

      if (installCommand.length > 0) {
        //  安装依赖
        await this.execCommand(installCommand, '依赖安装','依赖安装失败！')
        // 启动项目
        await this.execCommand(startCommand, '启动项目' ,'启动项目失败！')
      } else {
        //没有配置 安装依赖命令
        log.verbose('mongodb的template数据库里没有配置安装依赖命令')
      }
      
    }

    
  }

  // 自定义安装模版
  async customInstallTemplate() {
    log.verbose('自定义安装模版')
    if (await this.templateNpm.exists()) {
      const rootFile = this.templateNpm.getRootFilePath() // '/Users/xiejie/Desktop/test-custom' 
      console.log(555, rootFile)
      if (fs.existsSync(rootFile)) {
        log.notice('开始自定义安装模版')
        const templatePath = path.resolve(this.templateNpm.cacheFilePathPackageJson, 'template')
        const options = {
          templateInfo: this.templateInfo,
          projectInfo: this.projectInfo,
          sourcePath: templatePath,
          targetPath: process.cwd(),
        }
        const code = `require('${rootFile}')(${JSON.stringify(options)})`
        log.verbose('code', code)
        await execAsync('node', ['-e', code], { stdio: 'inherit', ced: process.cwd() })
        log.success('自定义模版安装成功')
      }
    }
  }

  // 模版list数据
  createTemplateChoices() {
    return this.template.map(item => ({
      value: item.npmName,
      name: item.name
    }))
  }

  // 检查 npm 命令是否正确
  checkCommand(cmd) {
    if (WHITE_COMMAND.includes(cmd)) {
      return cmd
    }
    return null
  }

  async execCommand(command, msg, errMsg) {
    log.verbose(`正在进行${msg}`)
    let ret
    if (command) {
      const cmdArray = command.split(' ')
      const cmd = this.checkCommand(cmdArray[0])
      if (!cmd) {
        throw new Error(`${msg}命令不存在！ 命令：` + command)
      }
      const args = cmdArray.slice(1)
      ret = await execAsync(cmd, args, {
        cwd: process.cwd(),//当前命令执行
        stdio: 'inherit',// 通过相应的标准输入输出流传入/传出父进程。 在前三个位置，这分别相当于 process.stdin、process.stdout 和 process.stderr。 在任何其他位置，相当于 'ignore'。
      })
    }

    if (ret === 0) {
     return ret
    } else {
      throw new Error(errMsg)
    }

  }

  ifDirIsEmpty(localPath) {
    
    let fileList = fs.readdirSync(localPath)
    // 文件过滤逻辑
    fileList = fileList.filter(file => (
      !file.startsWith('.') && ['node_modules'].indexOf(file) < 0
    ))
    return !fileList || fileList.length <= 0 
  }
}

module.exports.InitCommand = InitCommand

module.exports = init;

function init(argv) {
  // console.log('test init', projectName, options, process.env.CLI_TARGET_PATH)
  return new InitCommand(argv)
}
